<?php if(! defined('SYSPATH')) exit('No direct script access allowed');
/*
 +----------------------------------------------------------------------+
 | QuickPHP Framework Version 0.10                                      |
 +----------------------------------------------------------------------+
 | Copyright (c) 2010 QuickPHP.net All rights reserved.                 |
 +----------------------------------------------------------------------+
 | Licensed under the Apache License, Version 2.0 (the 'License');      |
 | you may not use this file except in compliance with the License.     |
 | You may obtain a copy of the License at                              |
 | http://www.apache.org/licenses/LICENSE-2.0                           |
 | Unless required by applicable law or agreed to in writing, software  |
 | distributed under the License is distributed on an 'AS IS' BASIS,    |
 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or      |
 | implied. See the License for the specific language governing         |
 | permissions and limitations under the License.                       |
 +----------------------------------------------------------------------+
 | Author: BoPo <ibopo@126.com>                                         |
 +----------------------------------------------------------------------+
*/
/**
 * Request and response wrapper. Uses the [Route] class to determine what
 * [Controller] to send the request to.
 *
 * @category    QuickPHP
 * @package     Request
 * @author      BoPo <ibopo@126.com>
 * @copyright   Copyright &copy; 2010 QuickPHP
 * @license     http://www.quickphp.net/license/
 * @version     $Id: Cache.php 7215 2011-01-20 15:47:40Z bopo $
 */
class QuickPHP_Request
{

    // HTTP status codes and messages
    public static $messages = array(
        // Informational 1xx
        100 => 'Continue',
        101 => 'Switching Protocols',

        // Success 2xx
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        207 => 'Multi-Status',

        // Redirection 3xx
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found', // 1.1
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        // 306 is deprecated but reserved
        307 => 'Temporary Redirect',

        // Client Error 4xx
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request-URI Too Long',
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',
        422 => 'Unprocessable Entity',
        423 => 'Locked',
        424 => 'Failed Dependency',

        // Server Error 5xx
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported',
        507 => 'Insufficient Storage',
        509 => 'Bandwidth Limit Exceeded'
    );

    /**
     * @var  string  method: GET, POST, PUT, DELETE, etc
     */
    public static $method = 'GET';

    /**
     * @var  string  protocol: http, https, ftp, cli, etc
     */
    public static $protocol = 'http';

    /**
     * @var  string  referring URL
     */
    public static $referrer;

    /**
     * @var  string  client user agent
     */
    public static $user_agent = '';

    /**
     * @var  string  client IP address
     */
    public static $client_ip = '0.0.0.0';

    /**
     * @var  boolean  AJAX-generated request
     */
    public static $is_ajax = FALSE;

    /**
     * @var  object  main request instance
     */
    public static $instance;

    /**
     * @var  object  currently executing request instance
     */
    public static $current;
    /**
     * @var  object  route matched for this request
     */
    public $route;

    /**
     * @var  integer  HTTP response code: 200, 404, 500, etc
     */
    public $status = 200;

    /**
     * @var  string  response body
     */
    public $response = '';

    /**
     * @var  array  headers to send with the response body
     */
    public $headers = array();

    /**
     * @var  string  controller directory
     */
    public $directory = '';

    /**
     * @var  string  controller to be executed
     */
    public $controller;

    /**
     * @var  string  action to be executed in the controller
     */
    public $action;

    /**
     * @var  string  the URI of the request
     */
    public $uri;

    // Parameters extracted from the route
    protected $_params;

    /**
     * Main request singleton instance. If no URI is provided, the URI will
     * be automatically detected using PATH_INFO, REQUEST_URI, or PHP_SELF.
     *
     *     $request = Request::instance();
     *
     * @param   string   URI of the request
     * @return  Request
     */
    public static function instance( $uri = TRUE)
    {
        if ( ! Request::$instance)
        {
            if (QuickPHP::$is_cli)
            {
                // Default protocol for command line is cli://
                Request::$protocol = 'cli';

                // Get the command line options
                $options = cli::options('uri', 'method', 'get', 'post');

                if (isset($options['uri']))
                {
                    // Use the specified URI
                    $uri = $options['uri'];
                }

                if (isset($options['method']))
                {
                    // Use the specified method
                    Request::$method = strtoupper($options['method']);
                }

                if (isset($options['get']))
                {
                    // Overload the global GET data
                    parse_str($options['get'], $_GET);
                }

                if (isset($options['post']))
                {
                    // Overload the global POST data
                    parse_str($options['post'], $_POST);
                }
            }
            else
            {
                if (isset($_SERVER['REQUEST_METHOD']))
                {
                    // Use the server request method
                    Request::$method = $_SERVER['REQUEST_METHOD'];
                }

                if ( ! empty($_SERVER['HTTPS']) AND filter_var($_SERVER['HTTPS'], FILTER_VALIDATE_BOOLEAN))
                {
                    // This request is secure
                    Request::$protocol = 'https';
                }

                if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) AND strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
                {
                    // This request is an AJAX request
                    Request::$is_ajax = TRUE;
                }

                if (isset($_SERVER['HTTP_REFERER']))
                {
                    // There is a referrer for this request
                    Request::$referrer = $_SERVER['HTTP_REFERER'];
                }

                if (isset($_SERVER['HTTP_USER_AGENT']))
                {
                    // Set the client user agent
                    Request::$user_agent = $_SERVER['HTTP_USER_AGENT'];
                }

                if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
                {
                    // Use the forwarded IP address, typically set when the
                    // client is using a proxy server.
                    Request::$client_ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
                }
                elseif (isset($_SERVER['HTTP_CLIENT_IP']))
                {
                    // Use the forwarded IP address, typically set when the
                    // client is using a proxy server.
                    Request::$client_ip = $_SERVER['HTTP_CLIENT_IP'];
                }
                elseif (isset($_SERVER['REMOTE_ADDR']))
                {
                    // The remote IP address
                    Request::$client_ip = $_SERVER['REMOTE_ADDR'];
                }

                if (Request::$method !== 'GET' AND Request::$method !== 'POST')
                {
                    // Methods besides GET and POST do not properly parse the form-encoded
                    // query string into the $_POST array, so we overload it manually.
                    parse_str(file_get_contents('php://input'), $_POST);
                }

                if ($uri === TRUE)
                {
                    if ( ! empty($_SERVER['PATH_INFO']))
                    {
                        // PATH_INFO does not contain the docroot or index
                        $uri = $_SERVER['PATH_INFO'];
                    }
                    else
                    {
                        // REQUEST_URI and PHP_SELF include the docroot and index

                        if (isset($_SERVER['REQUEST_URI']))
                        {
                            // REQUEST_URI includes the query string, remove it
                            $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

                            // Decode the request URI
                            $uri = rawurldecode($uri);
                        }
                        elseif (isset($_SERVER['PHP_SELF']))
                        {
                            $uri = $_SERVER['PHP_SELF'];
                        }
                        elseif (isset($_SERVER['REDIRECT_URL']))
                        {
                            $uri = $_SERVER['REDIRECT_URL'];
                        }
                        else
                        {
                            // If you ever see this error, please report an issue at http://dev.QuickPHPphp.com/projects/QuickPHP3/issues
                            // along with any relevant information about your web server setup. Thanks!
                            throw new QuickPHP_Exception('Unable to detect the URI using PATH_INFO, REQUEST_URI, or PHP_SELF');
                        }

                        // Get the path from the base URL, including the index file
                        $base_url = parse_url(QuickPHP::$base_url, PHP_URL_PATH);

                        if (strpos($uri, $base_url) === 0)
                        {
                            // Remove the base URL from the URI
                            $uri = substr($uri, strlen($base_url));
                        }

                        if (QuickPHP::$index_file AND strpos($uri, QuickPHP::$index_file) === 0)
                        {
                            // Remove the index file from the URI
                            $uri = substr($uri, strlen(QuickPHP::$index_file));
                        }
                    }
                }
            }

            // Reduce multiple slashes to a single slash
            $uri = preg_replace('#//+#', '/', $uri);

            // Remove all dot-paths from the URI, they are not valid
            $uri = preg_replace('#\.[\s./]*/#', '', $uri);

            // Create the instance singleton
            Request::$instance = Request::$current = new Request($uri);

            // Add the default Content-Type header
            Request::$instance->headers['Content-Type'] = 'text/html; charset='.QuickPHP::$charset;
        }

        return Request::$instance;
    }

    /**
     * Return the currently executing request. This is changed to the current
     * request when [Request::execute] is called and restored when the request
     * is completed.
     *
     *     $request = Request::current();
     *
     * @return  Request
     * @since   3.0.5
     */
    public static function current()
    {
        return Request::$current;
    }

    /**
     * Returns information about the client user agent.
     *
     *     // Returns "Chrome" when using Google Chrome
     *     $browser = Request::user_agent('browser');
     *
     * @param   string  value to return: browser, version, robot, mobile, platform
     * @return  string  requested information
     * @return  FALSE   no information found
     * @uses    QuickPHP::config
     * @uses    Request::$user_agent
     */
    public static function user_agent($value)
    {
        static $info;

        if (isset($info[$value]))
        {
            // This value has already been found
            return $info[$value];
        }

        if ($value === 'browser' OR $value == 'version')
        {
            // Load browsers
            $browsers = QuickPHP::config('user_agents')->browsers;

            foreach ($browsers as $search => $name)
            {
                if (stripos(Request::$user_agent, $search) !== FALSE)
                {
                    // Set the browser name
                    $info['browser'] = $name;

                    if (preg_match('#'.preg_quote($search).'[^0-9.]*+([0-9.][0-9.a-z]*)#i', Request::$user_agent, $matches))
                    {
                        // Set the version number
                        $info['version'] = $matches[1];
                    }
                    else
                    {
                        // No version number found
                        $info['version'] = FALSE;
                    }

                    return $info[$value];
                }
            }
        }
        else
        {
            // Load the search group for this type
            $group = QuickPHP::config('user_agents')->$value;

            foreach ($group as $search => $name)
            {
                if (stripos(Request::$user_agent, $search) !== FALSE)
                {
                    // Set the value name
                    return $info[$value] = $name;
                }
            }
        }

        // The value requested could not be found
        return $info[$value] = FALSE;
    }

    /**
     * Returns the accepted content types. If a specific type is defined,
     * the quality of that type will be returned.
     *
     *     $types = Request::accept_type();
     *
     * @param   string  content MIME type
     * @return  float   when checking a specific type
     * @return  array
     * @uses    Request::_parse_accept
     */
    public static function accept_type($type = NULL)
    {
        static $accepts;

        if ($accepts === NULL)
            $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT'], array('*/*' => 1.0));

        if (isset($type))
            return isset($accepts[$type]) ? $accepts[$type] : $accepts['*/*'];

        return $accepts;
    }

    /**
     * Returns the accepted languages. If a specific language is defined,
     * the quality of that language will be returned. If the language is not
     * accepted, FALSE will be returned.
     *
     *     $langs = Request::accept_lang();
     *
     * @param   string  language code
     * @return  float   when checking a specific language
     * @return  array
     * @uses    Request::_parse_accept
     */
    public static function accept_lang($lang = NULL)
    {
        static $accepts;

        if ($accepts === NULL)
            $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_LANGUAGE']);

        if (isset($lang))
            return isset($accepts[$lang]) ? $accepts[$lang] : FALSE;

        return $accepts;
    }

    /**
     * Returns the accepted encodings. If a specific encoding is defined,
     * the quality of that encoding will be returned. If the encoding is not
     * accepted, FALSE will be returned.
     *
     *     $encodings = Request::accept_encoding();
     *
     * @param   string  encoding type
     * @return  float   when checking a specific encoding
     * @return  array
     * @uses    Request::_parse_accept
     */
    public static function accept_encoding($type = NULL)
    {
        static $accepts;

        if ($accepts === NULL)
            $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_ENCODING']);

        if (isset($type))
            return isset($accepts[$type]) ? $accepts[$type] : FALSE;

        return $accepts;
    }

    /**
     * Parses an accept header and returns an array (type => quality) of the
     * accepted types, ordered by quality.
     *
     *     $accept = Request::_parse_accept($header, $defaults);
     *
     * @param   string   header to parse
     * @param   array    default values
     * @return  array
     */
    protected static function _parse_accept( & $header, array $accepts = NULL)
    {
        if ( ! empty($header))
        {
            // Get all of the types
            $types = explode(',', $header);

            foreach ($types as $type)
            {
                // Split the type into parts
                $parts = explode(';', $type);

                // Make the type only the MIME
                $type = trim(array_shift($parts));

                // Default quality is 1.0
                $quality = 1.0;

                foreach ($parts as $part)
                {
                    // Prevent undefined $value notice below
                    if (strpos($part, '=') === FALSE)
                        continue;

                    // Separate the key and value
                    list ($key, $value) = explode('=', trim($part));

                    if ($key === 'q')
                    {
                        // There is a quality for this type
                        $quality = (float) trim($value);
                    }
                }

                // Add the accept type and quality
                $accepts[$type] = $quality;
            }
        }

        // Make sure that accepts is an array
        $accepts = (array) $accepts;

        // Order by quality
        arsort($accepts);

        return $accepts;
    }


    /**
     * Creates a new request object for the given URI. New requests should be
     * created using the [Request::instance] or [Request::factory] methods.
     *
     *     $request = new Request($uri);
     *
     * @param   string  URI of the request
     * @return  void
     * @throws  QuickPHP_Request_Exception
     * @uses    Router::all
     * @uses    Router::matches
     */
    public function __construct()
    {

    }

    /**
     * Returns the response as the string representation of a request.
     *
     *     echo $request;
     *
     * @return  string
     */
    public function __toString()
    {
        return (string) $this->response;
    }

    /**
     * Retrieves a value from the route parameters.
     *
     *     $id = $request->param('id');
     *
     * @param   string   key of the value
     * @param   mixed    default value if the key is not set
     * @return  mixed
     */
    public function param($key = NULL, $default = NULL)
    {
        if ($key === NULL)
        {
            // Return the full array
            return $this->_params;
        }

        return isset($this->_params[$key]) ? $this->_params[$key] : $default;
    }

    /**
     * Sends the response status and all set headers. The current server
     * protocol (HTTP/1.0 or HTTP/1.1) will be used when available. If not
     * available, HTTP/1.1 will be used.
     *
     *     $request->headers();
     *
     * @return  $this
     * @uses    Request::$messages
     */
    public function headers()
    {
        if ( ! headers_sent())
        {
            if (isset($_SERVER['SERVER_PROTOCOL']))
            {
                // Use the default server protocol
                $protocol = $_SERVER['SERVER_PROTOCOL'];
            }
            else
            {
                // Default to using newer protocol
                $protocol = 'HTTP/1.1';
            }

            // HTTP status line
            header($protocol.' '.$this->status.' '.Request::$messages[$this->status]);

            foreach ($this->headers as $name => $value)
            {
                if (is_string($name))
                {
                    // Combine the name and value to make a raw header
                    $value = "{$name}: {$value}";
                }

                // Send the raw header
                header($value, TRUE);
            }
        }

        return $this;
    }

    /**
     * Send file download as the response. All execution will be halted when
     * this method is called! Use TRUE for the filename to send the current
     * response as the file content. The third parameter allows the following
     * options to be set:
     *
     * Type      | Option    | Description                        | Default Value
     * ----------|-----------|------------------------------------|--------------
     * `boolean` | inline    | Display inline instead of download | `FALSE`
     * `string`  | mime_type | Manual mime type                   | Automatic
     * `boolean` | delete    | Delete the file after sending      | `FALSE`
     *
     * Download a file that already exists:
     *
     *     $request->download('media/packages/QuickPHP.zip');
     *
     * Download generated content as a file:
     *
     *     $request->response = $content;
     *     $request->download(TRUE, $filename);
     *
     * [!!] No further processing can be done after this method is called!
     *
     * @param   string   filename with path, or TRUE for the current response
     * @param   string   downloaded file name
     * @param   array    additional options
     * @return  void
     * @throws  QuickPHP_Exception
     * @uses    file::mime_by_ext
     * @uses    file::mime
     * @uses    Request::send_headers
     */
    public function download($filename, $download = NULL, array $options = NULL)
    {
        if ( ! empty($options['mime_type']))
        {
            // The mime-type has been manually set
            $mime = $options['mime_type'];
        }

        if ($filename === TRUE)
        {
            if (empty($download))
            {
                throw new QuickPHP_Exception('Download name must be provided for streaming files');
            }

            // Temporary files will automatically be deleted
            $options['delete'] = FALSE;

            if ( ! isset($mime))
            {
                // Guess the mime using the file extension
                $mime = file::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION)));
            }

            // Get the content size
            $size = strlen($this->response);

            // Create a temporary file to hold the current response
            $file = tmpfile();

            // Write the current response into the file
            fwrite($file, $this->response);

            // Prepare the file for reading
            fseek($file, 0);
        }
        else
        {
            // Get the complete file path
            $filename = realpath($filename);

            if (empty($download))
            {
                // Use the file name as the download file name
                $download = pathinfo($filename, PATHINFO_BASENAME);
            }

            // Get the file size
            $size = filesize($filename);

            if ( ! isset($mime))
            {
                // Get the mime type
                $mime = file::mime($filename);
            }

            // Open the file for reading
            $file = fopen($filename, 'rb');
        }

        // Inline or download?
        $disposition = empty($options['inline']) ? 'attachment' : 'inline';

        // Set the headers for a download
        $this->headers['Content-Disposition'] = $disposition.'; filename="'.$download.'"';
        $this->headers['Content-Type']        = $mime;
        $this->headers['Content-Length']      = $size;

        if ( ! empty($options['resumable']))
        {
            // @todo: ranged download processing
        }

        // Send all headers now
        $this->headers();

        while (ob_get_level())
        {
            // Flush all output buffers
            ob_end_flush();
        }

        // Manually stop execution
        ignore_user_abort(TRUE);

        // Keep the script running forever
        set_time_limit(0);

        // Send data in 16kb blocks
        $block = 1024 * 16;

        while ( ! feof($file))
        {
            if (connection_aborted())
                break;

            // Output a block of the file
            echo fread($file, $block);

            // Send the data now
            flush();
        }

        // Close the file
        fclose($file);

        if ( ! empty($options['delete']))
        {
            try
            {
                // Attempt to remove the file
                unlink($filename);
            }
            catch (Exception $e)
            {
                // Create a text version of the exception
                $error = QuickPHP::exception_text($e);

                if (is_object(QuickPHP::$log))
                {
                    // Add this exception to the log
                    QuickPHP::$log->add(QuickPHP::ERROR, $error);

                    // Make sure the logs are written
                    QuickPHP::$log->write();
                }

                // Do NOT display the exception, it will corrupt the output!
            }
        }

        // Stop execution
        exit;
    }

    /**
     * Generates an [ETag](http://en.wikipedia.org/wiki/HTTP_ETag) from the
     * request response.
     *
     *     $etag = $request->generate_etag();
     *
     * [!!] If the request response is empty when this method is called, an
     * exception will be thrown!
     *
     * @return string
     * @throws QuickPHP_Request_Exception
     */
    public function generate_etag()
    {
        if ($this->response === NULL)
            throw new QuickPHP_Request_Exception('No response yet associated with request - cannot auto generate resource ETag');

        return '"'.sha1($this->response).'"';
    }


    /**
     * Checks the browser cache to see the response needs to be returned.
     *
     *     $request->check_cache($etag);
     *
     * [!!] If the cache check succeeds, no further processing can be done!
     *
     * @param   string  etag to check
     * @return  $this
     * @throws  QuickPHP_Request_Exception
     * @uses    Request::generate_etag
     */
    public function check_cache($etag = null)
    {
        if (empty($etag))
            $etag = $this->generate_etag();

        // Set the ETag header
        $this->headers['ETag'] = $etag;

        // Add the Cache-Control header if it is not already set
        // This allows etags to be used with Max-Age, etc
        $this->headers += array(
            'Cache-Control' => 'must-revalidate',
        );

        if (isset($_SERVER['HTTP_IF_NONE_MATCH']) AND $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
        {
            // No need to send data again
            $this->status = 304;
            $this->headers();

            // Stop execution
            exit;
        }

        return $this;
    }


    /**
     * 获取IP.
     *
     * @return string
     */
    public static function ip_address()
    {
        $keys = array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR');

        foreach ($keys as $key)
        {
            $ip = Request::server($key);

            if(! empty($ip))
            {
                $ip_address = $ip;
                break;
            }
        }

        $comma = strrpos($ip_address, ',');

        if($comma !== FALSE)
            $ip_address = substr($ip_address, $comma + 1);

        if(! valid::ip($ip_address))
            $ip_address = '0.0.0.0';

        return $ip_address;
    }
    /**
     * 从 $_SERVER 数组取一个指定项目.
     *
     * @param   string   键值
     * @param   mixed    默认值
     * @param   boolean  XSS clean 开关
     * @return  mixed
     */
    public static function server($key = array(), $default = NULL, $xss_clean = FALSE)
    {
        return Request::search_array($_SERVER, $key, $default, $xss_clean);
    }

    protected static function search_array($array, $key, $default = NULL, $xss_clean = FALSE)
    {
        if($key === array()) return $array;
        if(! isset($array[$key])) return $default;

        $value = $array[$key];

        if($xss_clean === TRUE)
            $value = security::xss_clean($value);

        return $value;
    }
    /**
     * 从  $_POST 数组取一个指定项目.
     *
     * @param   string   键值
     * @param   mixed    默认值
     * @param   boolean  XSS clean 开关
     * @return  mixed
     */
    public static function post($key = array(), $default = NULL, $xss_clean = FALSE)
    {
        return Request::search_array($_POST, $key, $default, $xss_clean);
    }

    /**
     * 从 $_COOKIE 数组取一个指定项目.
     *
     * @param   string   键值
     * @param   mixed    默认值
     * @param   boolean  XSS clean 开关
     * @return  mixed
     */
    public static function cookie($key = array(), $default = NULL, $xss_clean = FALSE)
    {
        return Request::search_array($_COOKIE, $key, $default, $xss_clean);
    }
}
